"use strict";

var $ = require("jquery"),
    eventEmitterModule = require("viz/vector_map/event_emitter"),
    projectionModule = require("viz/vector_map/projection.main"),
    Projection = projectionModule.Projection,
    projectionEnginesModule = require("viz/vector_map/projection"),
    projection = projectionEnginesModule.projection;

function returnValue(value) {
    return function() {
        return value;
    };
}

var environment = {
    beforeEach: function() {
        this.centerChanged = sinon.spy();
        this.zoomChanged = sinon.spy();
        this.projection = new Projection({
            centerChanged: this.centerChanged,
            zoomChanged: this.zoomChanged
        });
        this.engine = createStubEngine();
        this.projection.setEngine(this.engine);
        this.projection.setCenter([20, -10]);
        this.projection.setZoom(2);
        this.centerChanged.reset();
        this.zoomChanged.reset();
    },

    afterEach: function() {
        this.projection.dispose();
    }
};

QUnit.module("General", environment);

QUnit.test("setEngine / center is changed", function(assert) {
    var onEngine = sinon.spy(),
        onScreen = sinon.spy(),
        onCenter = sinon.spy(),
        onZoom = sinon.spy(),
        engine = projection({
            to: function(coords) {
                return [
                    (coords[0] + 60) / 20,
                    (coords[1] - 40) / 10
                ];
            },
            from: function(coords) {
                return [
                    (coords[0] - 3) * 20,
                    (coords[1] + 4) * 10
                ];
            }
        });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ engine: onEngine, screen: onScreen, center: onCenter, zoom: onZoom });

    this.projection.setEngine(engine);

    assert.strictEqual(onEngine.callCount, 1, "engine event is triggered");
    assert.strictEqual(onScreen.callCount, 1, "screen event is triggered");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered");
    assert.strictEqual(onZoom.callCount, 1, "zoom event is triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[-60, 40]], "centerChanged is called");
    assert.deepEqual(this.zoomChanged.lastCall.args, [1], "zoomChanged is called");
    assert.deepEqual(this.projection.getCenter(), [-60, 40], "center");
    assert.strictEqual(this.projection.getZoom(), 1, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 0, translateY: 0 }, "transform");
});

QUnit.test("setEngine / center is not changed", function(assert) {
    var onEngine = sinon.spy(),
        onScreen = sinon.spy(),
        onCenter = sinon.spy(),
        onZoom = sinon.spy(),
        engine = projection({
            to: function(coords) {
                return [
                    (coords[0] - 40) / 40,
                    (coords[1] + 20) / 10
                ];
            },
            from: function(coords) {
                return [
                    (coords[0] + 1) * 40,
                    (coords[1] - 2) * 10
                ];
            }
        });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.setCenter(engine.center());
    this.projection.setZoom(1);
    this.centerChanged.reset();
    this.zoomChanged.reset();
    this.projection.on({ engine: onEngine, screen: onScreen, center: onCenter, zoom: onZoom });

    this.projection.setEngine(engine);

    assert.strictEqual(onEngine.callCount, 1, "engine event is triggered");
    assert.strictEqual(onScreen.callCount, 1, "screen event is triggered");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(onZoom.callCount, 0, "zoom event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
    assert.strictEqual(this.zoomChanged.lastCall, null, "zoomChanged is not called");
    assert.deepEqual(this.projection.getCenter(), [40, -20], "center");
    assert.strictEqual(this.projection.getZoom(), 1, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 0, translateY: 0 }, "transform");
});

QUnit.test("setEngine / not changed", function(assert) {
    var onEngine = sinon.spy(),
        onScreen = sinon.spy(),
        onCenter = sinon.spy(),
        onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ engine: onEngine, screen: onScreen, center: onCenter, zoom: onZoom });

    this.projection.setEngine(this.engine);

    assert.strictEqual(onEngine.callCount, 0, "engine event is not triggered");
    assert.strictEqual(onScreen.callCount, 0, "screen event is not triggered");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(onZoom.callCount, 0, "zoom event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
    assert.strictEqual(this.zoomChanged.lastCall, null, "zoomChanged is not called");
    assert.deepEqual(this.projection.getCenter(), [20, -10], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
});

QUnit.test("setEngine / non invertible", function(assert) {
    var onEngine = sinon.spy(),
        onScreen = sinon.spy(),
        onCenter = sinon.spy(),
        onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ engine: onEngine, screen: onScreen, center: onCenter, zoom: onZoom });

    this.projection.setEngine(projection({
        to: returnValue([])
    }));

    assert.strictEqual(onEngine.callCount, 1, "engine event is triggered");
    assert.strictEqual(onScreen.callCount, 1, "screen event is triggered");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered");
    assert.strictEqual(onZoom.callCount, 1, "zoom event is triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[NaN, NaN]], "centerChanged is called");
    assert.deepEqual(this.zoomChanged.lastCall.args, [1], "zoomChanged is called");
    assert.deepEqual(this.projection.getCenter(), [NaN, NaN], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 0, translateY: 0 }, "transform");
});

QUnit.test("Event emitter methods are injected", function(assert) {
    var projection = this.projection;
    $.each(eventEmitterModule._TESTS_eventEmitterMethods, function(name, method) {
        assert.strictEqual(projection[name], method, name);
    });
});

QUnit.test("setSize", function(assert) {
    var onScreen = sinon.spy();
    this.projection.on({ screen: onScreen });

    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onScreen.callCount, 1, "screen event is triggered");

    this.projection.setSize({ left: 100, top: 200, width: 600, height: 800 });

    assert.deepEqual(this.projection.getTransform(), { translateX: 600, translateY: 750 }, "transform");
    assert.strictEqual(onScreen.callCount, 2, "screen event is triggered");
});

QUnit.test("isInvertible", function(assert) {
    this.engine.isInvertible = returnValue("test");

    assert.strictEqual(this.projection.isInvertible(), "test");
});

QUnit.test("setBounds", function(assert) {
    var engine1 = { tag: "engine-1" },
        engine2 = { tag: "engine-2" };
    this.projection.setEngine = sinon.spy();
    this.engine.original = sinon.stub().returns(engine1);
    engine1.bounds = sinon.stub().returns(engine2);

    this.projection.setBounds("bounds");

    assert.deepEqual(this.engine.original.lastCall.args, [], "original");
    assert.deepEqual(engine1.bounds.lastCall.args, ["bounds"], "bounds");
    assert.deepEqual(this.projection.setEngine.lastCall.args, [engine2], "engine");
});

QUnit.test("setBounds / no argument", function(assert) {
    this.projection.setEngine = sinon.spy();
    this.engine.original = sinon.spy();

    this.projection.setBounds();

    assert.strictEqual(this.projection.setEngine.lastCall, null, "engine");
    assert.strictEqual(this.engine.original.lastCall, null, "original");
});

QUnit.test("getTransform / zooming", function(assert) {
    this.projection.setZoom(3);

    this.engine.ar = returnValue(0.5);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 300, translateY: 600 }, "aspect ratio is 0.5");

    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 900, translateY: 450 }, "aspect ratio is 2");
});

QUnit.test("getTransform / centering", function(assert) {
    this.projection.setCenter([30, -15]);

    this.engine.ar = returnValue(0.5);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 100, translateY: 200 }, "aspect ratio is 0.5");

    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 300, translateY: 150 }, "aspect ratio is 2");
});

QUnit.test("getTransform / zooming and centering", function(assert) {
    this.projection.setZoom(3);
    this.projection.setCenter([30, -15]);

    this.engine.ar = returnValue(0.5);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 150, translateY: 300 }, "aspect ratio is 0.5");

    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 200, top: 100, width: 600, height: 400 });
    assert.deepEqual(this.projection.getTransform(), { translateX: 450, translateY: 225 }, "aspect ratio is 2");
});

QUnit.test("getCenter", function(assert) {
    assert.deepEqual(this.projection.getCenter(), [20, -10]);
});

QUnit.test("setCenter", function(assert) {
    var onCenter = sinon.spy();
    this.projection.on({ center: onCenter });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenter([30, -15]);

    assert.deepEqual(this.projection.getCenter(), [30, -15], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 280, translateY: 350 }, "transform");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[30, -15]], "centerChanged is called");
});

QUnit.test("setCenter / not changed", function(assert) {
    var onCenter = sinon.spy();
    this.projection.on({ center: onCenter });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenter([20, -10]);

    assert.deepEqual(this.projection.getCenter(), [20, -10], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
});

QUnit.test("setCenter / out of bounds", function(assert) {
    var onCenter = sinon.spy();
    this.projection.on({ center: onCenter });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenter([80, -40]);

    assert.deepEqual(this.projection.getCenter(), [60, -30], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: -560, translateY: -700 }, "transform");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[60, -30]], "centerChanged is called");
});

QUnit.test("setCenter / not valid", function(assert) {
    var onCenter = sinon.spy();
    this.projection.on({ center: onCenter });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenter("test");

    assert.deepEqual(this.projection.getCenter(), [40, -20], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 0, translateY: 0 }, "transform");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[40, -20]], "centerChanged is called");
});

QUnit.test("setCenter / engine is not invertible", function(assert) {
    var onCenter = sinon.spy();
    this.engine.isInvertible = returnValue(false);
    this.projection.on({ center: onCenter });
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenter([10, 20]);

    assert.deepEqual(this.projection.getCenter(), [20, -10], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
});

QUnit.test("setCenterByPoint", function(assert) {
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    this.projection.setCenterByPoint([30, -15], [740, 100]);

    assert.deepEqual(this.projection.getCenter(), [25, -20], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 420, translateY: 0 }, "transform");
});

QUnit.test("moveCenter", function(assert) {
    var onCenter = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ center: onCenter });

    this.projection.beginMoveCenter();

    this.projection.moveCenter([140, 0]);
    assert.deepEqual(this.projection.getCenter(), [25, -10], "center 1");
    assert.deepEqual(this.projection.getTransform(), { translateX: 420, translateY: 700 }, "transform 1");
    assert.strictEqual(onCenter.callCount, 1, "center event is triggered 1");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called 1");

    this.projection.moveCenter([0, 140]);
    assert.deepEqual(this.projection.getCenter(), [25, -12], "center 2");
    assert.deepEqual(this.projection.getTransform(), { translateX: 420, translateY: 560 }, "transform 2");
    assert.strictEqual(onCenter.callCount, 2, "center event is triggered 2");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called 2");

    this.projection.moveCenter([280, 210]);
    assert.deepEqual(this.projection.getCenter(), [35, -15], "center 3");
    assert.deepEqual(this.projection.getTransform(), { translateX: 140, translateY: 350 }, "transform 3");
    assert.strictEqual(onCenter.callCount, 3, "center event is triggered 3");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called 3");

    this.projection.endMoveCenter();

    assert.strictEqual(onCenter.callCount, 3, "center event is not triggered");
    assert.deepEqual(this.centerChanged.lastCall.args, [[35, -15]], "centerChanged is called");
});

QUnit.test("moveCenter / without begin and end", function(assert) {
    var onCenter = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ center: onCenter });

    this.projection.moveCenter([100, 200]);

    assert.deepEqual(this.projection.getCenter(), [20, -10], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
});

QUnit.test("moveCenter / non invertible", function(assert) {
    var onCenter = sinon.spy();
    this.engine.isInvertible = returnValue(false);
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ center: onCenter });

    this.projection.beginMoveCenter();
    this.projection.moveCenter([100, 200]);
    this.projection.endMoveCenter();

    assert.deepEqual(this.projection.getCenter(), [20, -10], "center");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onCenter.callCount, 0, "center event is not triggered");
    assert.strictEqual(this.centerChanged.lastCall, null, "centerChanged is not called");
});

QUnit.test("getZoom", function(assert) {
    assert.strictEqual(this.projection.getZoom(), 2);
});

QUnit.test("setZoom", function(assert) {
    var onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ zoom: onZoom });

    this.projection.setZoom(3);

    assert.strictEqual(this.projection.getZoom(), 3, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 840, translateY: 1050 }, "transform");
    assert.strictEqual(onZoom.callCount, 1, "zoom event is triggered");
    assert.deepEqual(this.zoomChanged.lastCall.args, [3], "zoomChanged is called");
});

QUnit.test("setZoom / not changed", function(assert) {
    var onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.setZoom(3);
    this.zoomChanged.reset();
    this.projection.on({ zoom: onZoom });

    this.projection.setZoom(3);

    assert.strictEqual(this.projection.getZoom(), 3, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 840, translateY: 1050 }, "transform");
    assert.strictEqual(onZoom.callCount, 0, "zoom event is not triggered");
    assert.strictEqual(this.zoomChanged.lastCall, null, "zoomChanged is not called");
});

QUnit.test("setZoom / out of bounds", function(assert) {
    var onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.setMaxZoom(5);
    this.zoomChanged.reset();
    this.projection.on({ zoom: onZoom });

    this.projection.setZoom(10);

    assert.strictEqual(this.projection.getZoom(), 5, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 1400, translateY: 1750 }, "transform");
    assert.strictEqual(onZoom.callCount, 1, "zoom event is triggered");
    assert.deepEqual(this.zoomChanged.lastCall.args, [5], "zoomChanged is called");
});

QUnit.test("setZoom / not valid", function(assert) {
    var onZoom = sinon.spy();
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.setZoom(3);
    this.zoomChanged.reset();
    this.projection.on({ zoom: onZoom });

    this.projection.setZoom("test");

    assert.strictEqual(this.projection.getZoom(), 1, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 280, translateY: 350 }, "transform");
    assert.strictEqual(onZoom.callCount, 1, "zoom event is triggered");
    assert.deepEqual(this.zoomChanged.lastCall.args, [1], "zoomChanged is called");
});

QUnit.test("setZoom / engine is not invertible", function(assert) {
    var onZoom = sinon.spy();
    this.engine.isInvertible = returnValue(false);
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    this.projection.on({ zoom: onZoom });

    this.projection.setZoom(3);

    assert.strictEqual(this.projection.getZoom(), 2, "zoom");
    assert.deepEqual(this.projection.getTransform(), { translateX: 560, translateY: 700 }, "transform");
    assert.strictEqual(onZoom.callCount, 0, "zoom event is not triggered");
    assert.strictEqual(this.zoomChanged.lastCall, null, "zoomChanged is not called");
});

QUnit.test("getScaledZoom", function(assert) {
    this.projection.setMaxZoom(10);
    this.projection.setZoom(1);
    assert.strictEqual(this.projection.getScaledZoom(), 0, "max zoom is 10, zoom is 1");
    this.projection.setZoom(4);
    assert.strictEqual(this.projection.getScaledZoom(), 4, "max zoom is 10, zoom is 4");
    this.projection.setZoom(10);
    assert.strictEqual(this.projection.getScaledZoom(), 7, "max zoom is 10, zoom is 10");

    this.projection.setMaxZoom(30);
    this.projection.setZoom(1);
    assert.strictEqual(this.projection.getScaledZoom(), 0, "max zoom is 30, zoom is 1");
    this.projection.setZoom(18);
    assert.strictEqual(this.projection.getScaledZoom(), 8, "max zoom is 30, zoom is 18");
    this.projection.setZoom(30);
    assert.strictEqual(this.projection.getScaledZoom(), 10, "max zoom is 30, zoom is 30");
});

QUnit.test("setScaledZoom", function(assert) {
    var setZoom = sinon.spy(this.projection, "setZoom");

    this.projection.setMaxZoom(10);
    this.projection.setScaledZoom(0);
    checkZoom(1, "max zoom is 10, scaled zoom is 0 - zoom");
    this.projection.setScaledZoom(4);
    checkZoom(3.7276, "max zoom is 10, scaled zoom is 4 - zoom");
    this.projection.setScaledZoom(7);
    checkZoom(10, "max zoom is 10, scaled zoom is 7 - zoom");

    this.projection.setMaxZoom(30);
    this.projection.setScaledZoom(0);
    checkZoom(1, "max zoom is 30, scaled zoom is 0 - zoom");
    this.projection.setScaledZoom(8);
    checkZoom(15.1949, "max zoom is 30, scaled zoom is 8 - zoom");
    this.projection.setScaledZoom(10);
    checkZoom(30, "max zoom is 30, scaled zoom is 30 - zoom");

    function checkZoom(expected, message) {
        assert.roughEqual(setZoom.lastCall.args[0], expected, 1E-4, message);
    }
});

QUnit.test("changeScaledZoom", function(assert) {
    var setZoom = sinon.spy(this.projection, "setZoom");
    this.projection.setMaxZoom(10);

    this.projection.changeScaledZoom(-1);
    checkZoom(1.3895, "-1");
    this.projection.changeScaledZoom(3);
    checkZoom(3.7276, "3");
    this.projection.changeScaledZoom(100);
    checkZoom(10, "100");
    this.projection.changeScaledZoom(-50);
    checkZoom(1, "-50");

    function checkZoom(expected, message) {
        assert.roughEqual(setZoom.lastCall.args[0], expected, 1E-4, message);
    }
});

QUnit.test("getZoomScalePartition", function(assert) {
    this.projection.setMaxZoom(32);
    assert.strictEqual(this.projection.getZoomScalePartition(), 10, "32 (default)");
    this.projection.setMaxZoom(200);
    assert.strictEqual(this.projection.getZoomScalePartition(), 15, "200");
    this.projection.setMaxZoom(1 << 20);
    assert.strictEqual(this.projection.getZoomScalePartition(), 40, "2^20");
    this.projection.setMaxZoom(90000);
    assert.strictEqual(this.projection.getZoomScalePartition(), 33, "90000");
    this.projection.setMaxZoom(9);
    assert.strictEqual(this.projection.getZoomScalePartition(), 6, "9");
    this.projection.setMaxZoom(4);
    assert.strictEqual(this.projection.getZoomScalePartition(), 4, "4");
    this.projection.setMaxZoom(2);
    assert.strictEqual(this.projection.getZoomScalePartition(), 4, "2");
});

QUnit.test("setMaxZoom", function(assert) {
    var onMaxZoom = sinon.spy();

    sinon.spy(this.projection, "setZoom");
    this.projection.on({ "max-zoom": onMaxZoom });

    this.projection.setMaxZoom(20);

    assert.strictEqual(onMaxZoom.callCount, 1, "max-zoom event is triggered");
    assert.strictEqual(this.projection.setZoom.lastCall, null, "zoom is not changed");
});

QUnit.test("setMaxZoom / less than current zoom", function(assert) {
    this.projection.setZoom(10);
    var onMaxZoom = sinon.spy();

    sinon.spy(this.projection, "setZoom");
    this.projection.on({ "max-zoom": onMaxZoom });

    this.projection.setMaxZoom(5);

    assert.strictEqual(onMaxZoom.callCount, 1, "max-zoom event is triggered");
    assert.deepEqual(this.projection.setZoom.lastCall.args, [5], "zoom is changed");
});

QUnit.test("setMaxZoom / not valid", function(assert) {
    var onMaxZoom = sinon.spy();

    sinon.spy(this.projection, "setZoom");
    this.projection.on({ "max-zoom": onMaxZoom });

    this.projection.setMaxZoom("test");

    assert.strictEqual(onMaxZoom.callCount, 1, "max-zoom event is triggered");
    assert.strictEqual(this.projection.setZoom.lastCall, null, "zoom is not changed");
});

QUnit.test("getViewport", function(assert) {
    assert.deepEqual(this.projection.getViewport(), [10, -15, 30, -5], "viewport");
});

QUnit.test("setViewport", function(assert) {
    var setCenter = sinon.spy(this.projection, "setCenter"),
        setZoom = sinon.spy(this.projection, "setZoom");

    this.projection.setViewport([-10, 20, 20, -5]);

    assert.deepEqual(setCenter.lastCall.args, [[5, 7.5]], "setCenter");
    assert.deepEqual(setZoom.lastCall.args, [0.8], "setZoom");
});

QUnit.test("setViewport / not valid", function(assert) {
    var setCenter = sinon.spy(this.projection, "setCenter"),
        setZoom = sinon.spy(this.projection, "setZoom");

    this.projection.setViewport(null);

    assert.deepEqual(setCenter.lastCall.args, [[40, -20]], "setCenter");
    assert.deepEqual(setZoom.lastCall.args, [1], "setZoom");
});

QUnit.test("fromScreenPoint", function(assert) {
    this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });

    assert.deepEqual(this.projection.fromScreenPoint([670, 275]), [22.5, -7.5]);
});

// TODO: Use fake engine!
QUnit.module('Viewport', {
    beforeEach: function() {
        this.projection1 = this._create();
        this.projection2 = this._create();
    },

    _create: function() {
        var projection = new Projection({ centerChanged: $.noop, zoomChanged: $.noop });
        projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
        return projection;
    },

    afterEach: function() {
        this.projection1.dispose();
        this.projection2.dispose();
    },

    setBounds: function(bounds) {
        this.projection1.setBounds(bounds);
        this.projection2.setBounds(bounds);
        this.projection1.setCenter(null);
        this.projection2.setCenter(null);
    }
});

$.each([null, [-180, 90, 180, -20], [-50, 90, 180, -90], [-180, 20, 180, -90], [-180, 90, 50, -90]], function(_, bounds) {
    var namePart = bounds ? '[' + bounds.join(', ') + ']' : 'null';
    namePart = ' / ' + namePart;

    QUnit.test('Common - to viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setZoom(2);
        this.projection1.setCenter([50, 30]);

        this.projection2.setViewport(this.projection1.getViewport());

        assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
        assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    });

    QUnit.test('Common - from viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setViewport([-40, 76.3123, 140, -50.3964]);

        this.projection2.setCenter(this.projection1.getCenter());
        this.projection2.setZoom(this.projection1.getZoom());

        assert.arraysEqual(this.projection2.getViewport(), this.projection1.getViewport());
    });

    //QUnit.test('Northern edge - to viewport' + namePart, function (assert) {
    //    this.setBounds(bounds);
    //
    //    this.projection1.setZoom(2).setCenter([0, 80]);
    //
    //    this.projection2.setViewport(this.projection1.getViewport());
    //
    //    assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
    //    assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    //});

    QUnit.test('Northern edge - from viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setViewport([-90, 85.0511, 90, 44.3512]);

        this.projection2.setCenter(this.projection1.getCenter());
        this.projection2.setZoom(this.projection1.getZoom());

        assert.arraysEqual(this.projection2.getViewport(), this.projection1.getViewport());
    });

    //QUnit.test('Eastern edge - to viewport' + namePart, function (assert) {
    //    this.setBounds(bounds);
    //
    //    this.projection1.setZoom(1.75).setCenter([170, 15]);
    //
    //    this.projection2.setViewport(this.projection1.getViewport());
    //
    //    assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
    //    assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    //});

    QUnit.test('Eastern edge - from viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setViewport([67.1429, 75.4737, 180, -65.5725]);

        this.projection2.setCenter(this.projection1.getCenter());
        this.projection2.setZoom(this.projection1.getZoom());

        assert.arraysEqual(this.projection2.getViewport(), this.projection1.getViewport());
    });

    //QUnit.test('Southern edge - to viewport' + namePart, function (assert) {
    //    this.setBounds(bounds);
    //
    //    this.projection1.setZoom(2.3).setCenter([50, -75]);
    //
    //    this.projection2.setViewport(this.projection1.getViewport());
    //
    //    assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
    //    assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    //});

    QUnit.test('Southern edge - from viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setViewport([-28.2609, -35.414, 128.2609, -85.0511]);

        this.projection2.setCenter(this.projection1.getCenter());
        this.projection2.setZoom(this.projection1.getZoom());

        assert.arraysEqual(this.projection2.getViewport(), this.projection1.getViewport());
    });

    //QUnit.test('Western edge - to viewport' + namePart, function (assert) {
    //    this.setBounds(bounds);
    //
    //    this.projection1.setZoom(4.75).setCenter([-160, -10]);
    //
    //    this.projection2.setViewport(this.projection1.getViewport());
    //
    //    assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
    //    assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    //});

    QUnit.test('Western edge - from viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection1.setViewport([-180, 26.8082, -122.1053, -43.1663]);

        this.projection2.setCenter(this.projection1.getCenter());
        this.projection2.setZoom(this.projection1.getZoom());

        assert.arraysEqual(this.projection2.getViewport(), this.projection1.getViewport());
    });

    // T136620
    QUnit.test('Reset viewport' + namePart, function(assert) {
        this.setBounds(bounds);

        this.projection2.setCenter([100, 50]);
        this.projection2.setZoom(3.2);
        this.projection2.setViewport(null);

        assert.roughEqual(this.projection2.getZoom(), this.projection1.getZoom(), 1E-8, 'zoom');
        assert.arraysEqual(this.projection2.getCenter(), this.projection1.getCenter(), 'center');
    });
});

QUnit.test('Longitude range is less than latitude range - at center', function(assert) {
    this.projection1.setViewport([-40, 20, 40, -20]);

    assert.arraysEqual(this.projection1.getViewport(), [-40, -37.098, 40, 37.098]);
});

QUnit.test('Longitude range is greater than latitude range - at center', function(assert) {
    this.projection1.setViewport([-10, 20, 10, -20]);

    assert.arraysEqual(this.projection1.getViewport(), [-20.419, -20, 20.419, 20]);
});

QUnit.test('Longitude range is less than latitude range - not at center', function(assert) {
    this.projection1.setViewport([30, 40, 120, -30]);

    assert.arraysEqual(this.projection1.getViewport(), [30, -36.2, 120, 45.4369]);
});

QUnit.test('Longitude range is greater than latitude range - not at center', function(assert) {
    this.projection1.setViewport([-100, 80, -40, 10]);

    assert.arraysEqual(this.projection1.getViewport(), [-134.7677, 10, -5.2323, 80]);
});

QUnit.module("Project", $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.engine.project = function(arg) {
            return "#" + arg;
        };
    }
}));

QUnit.test("Project", function(assert) {
    assert.deepEqual(this.projection.project("a"), "#a");
});

QUnit.module("Coordinates", $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
        this.projection.setZoom(4);
    }
}));

QUnit.test("Transform", function(assert) {
    assert.deepEqual(this.projection.transform([10, 20]), [11800, 28450]);
});

// TODO: Remove cases with bounds when "projection" module is considered stable
QUnit.module('Mercator - project', {
    beforeEach: function() {
        this.engine = projection.get("mercator");
    },

    doTest: function(assert, arg, expected) {
        var actual = this.engine.project(arg);
        assert.arraysEqual(actual, expected, arg.join(" "));

    },

    bounds: function(bounds) {
        this.engine = this.engine.bounds(bounds);
    }
});

QUnit.test('Common', function(assert) {
    this.doTest(assert, [-180, -85.0511], [-1, 1]);
    this.doTest(assert, [180, 85.0511], [1, -1]);
    this.doTest(assert, [0, 0], [0, 0]);
    this.doTest(assert, [-90, 40], [-0.5, -0.24284168]);
    this.doTest(assert, [90, -40], [0.5, 0.24284168]);
    this.doTest(assert, [-200, 100], [-1.1111, -1]);
    this.doTest(assert, [200, -100], [1.1111, 1]);
});

QUnit.test('With bounds / width < height', function(assert) {
    this.bounds([10, 80, 100, 10]);
    this.doTest(assert, [10, 10], [-1, 1.43928286]);
    this.doTest(assert, [-20, 0], [-1.66666667, 1.66264196]);
    this.doTest(assert, [100, 80], [1, -1.43928286]);
    this.doTest(assert, [150, 90], [2.11111111, -2.33735804]);
    this.doTest(assert, [55, 45], [0, 0.54044226]);
});

QUnit.test('With bounds / width > height', function(assert) {
    this.bounds([-80, 40, 60, -10]);
    this.doTest(assert, [-80, -10], [-2.60403768, 1]);
    this.doTest(assert, [60, 40], [2.60403768, -1]);
    this.doTest(assert, [-100, -50], [-3.34804845, 2.7802958]);
    this.doTest(assert, [90, 70], [3.72005383, -3.07283116]);
    this.doTest(assert, [-10, 25], [0, -0.33491949]);
});

// T124366, T224204
QUnit.test('With bounds / width is zero', function(assert) {
    this.bounds([10, 40, 10, -50]);
    this.doTest(assert, [10, 30], [0, -0.75912904]);
});

// T124366, T224204
QUnit.test('With bounds / height is zero', function(assert) {
    this.bounds([-20, 40, 100, 40]);
    this.doTest(assert, [20, 40], [-0.33333333, 0]);
});

// Remove cases with bounds when "projection" module is considered stable
QUnit.module('Mercator - unproject', {
    beforeEach: function() {
        this.engine = projection.get("mercator");
    },

    doTest: function(assert, arg, expected) {
        var actual = this.engine.unproject(arg);
        assert.arraysEqual(actual, expected, arg.join(" "));
    },

    bounds: function(bounds) {
        this.engine = this.engine.bounds(bounds);
    }
});

QUnit.test('Common', function(assert) {
    this.doTest(assert, [-1, 1], [-180, -85.0511]);
    this.doTest(assert, [1, -1], [180, 85.0511]);
    this.doTest(assert, [0, 0], [0, 0]);
    this.doTest(assert, [-0.5, -0.24284168], [-90, 39.99999938]);
    this.doTest(assert, [0.5, 0.24284168], [90, -39.99999938]);
    this.doTest(assert, [-2, 3], [-360, -89.9908]);
    this.doTest(assert, [2, -3], [360, 89.9908]);
});

QUnit.test('With bounds / width < height', function(assert) {
    this.bounds([10, 80, 100, 10]);
    this.doTest(assert, [-1, 1.43928286], [10, 9.99999984]);
    this.doTest(assert, [-1.66666667, 1.66264196], [-20.00000015, 0]);
    this.doTest(assert, [1, -1.43928286], [100, 80.00000003]);
    this.doTest(assert, [2.11111111, -2.33735804], [149.99999995, 85.05112878]);
    this.doTest(assert, [0, 0.54044226], [55, 44.99999985]);
});

QUnit.test('With bounds / width > height', function(assert) {
    this.bounds([-80, 40, 60, -10]);
    this.doTest(assert, [-2.60403768, 1], [-79.99999995, -10]);
    this.doTest(assert, [2.60403768, -1], [59.99999995, 40]);
    this.doTest(assert, [-3.34804845, 2.7802958], [-100.00000004, -49.99999998]);
    this.doTest(assert, [3.72005383, -3.07283116], [89.99999996, 70.00000004]);
    this.doTest(assert, [0, -0.33491949], [-10, 25.00000009]);
});

// T124366, T224204
QUnit.test('With bounds / width is zero', function(assert) {
    this.bounds([10, 40, 10, -50]);
    this.doTest(assert, [0, -0.75912904], [10, 30.00000013]);
});

// T124366, T224204
QUnit.test('With bounds / height is zero', function(assert) {
    this.bounds([-20, 40, 100, 40]);
    this.doTest(assert, [-0.33333333, 0], [20.0000002, 40]);
});

QUnit.module('Internal - to transformed', $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.projection.setZoom(null);
    },

    doTest: createInternalMethodTester("_toTransformed")
}));

QUnit.test("Default", function(assert) {
    this.doTest(assert, [-1, 1], [0, 2]);
    this.doTest(assert, [0, 0], [1, 1]);
    this.doTest(assert, [0.5, -0.8], [1.5, 0.2]);
});

QUnit.test("Zoom", function(assert) {
    this.projection.setZoom(2);
    this.doTest(assert, [-1, 1], [0, 4]);
    this.doTest(assert, [0, 0], [2, 2]);
    this.doTest(assert, [0.5, -0.8], [3, 0.4]);
});

QUnit.test("Center", function(assert) {
    this.projection.setCenter([10, 5]);
    this.doTest(assert, [1, -1], [2, 0]);
    this.doTest(assert, [0, 0], [1, 1]);
    this.doTest(assert, [-0.5, 0.4], [0.5, 1.4]);
});

QUnit.test("Center and zoom", function(assert) {
    this.projection.setCenter([10, 5]);
    this.projection.setZoom(2);
    this.doTest(assert, [1, 1], [4, 4]);
    this.doTest(assert, [-0.2, -0.2], [1.6, 1.6]);
    this.doTest(assert, [0, 0], [2, 2]);
});

QUnit.module("Internal - from transformed", $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.projection.setZoom(null);
    },

    doTest: createInternalMethodTester("_fromTransformed")
}));

QUnit.test("Default", function(assert) {
    this.doTest(assert, [-2, 2], [-3, 1]);
    this.doTest(assert, [0, 0], [-1, -1]);
    this.doTest(assert, [1, -1.6], [0, -2.6]);
});

QUnit.test("Zoom", function(assert) {
    this.projection.setZoom(2);
    this.doTest(assert, [-2, 2], [-2, 0]);
    this.doTest(assert, [0, 0], [-1, -1]);
    this.doTest(assert, [1, -1.6], [-0.5, -1.8]);
});

QUnit.test("Center", function(assert) {
    this.projection.setCenter([10, 5]);
    this.doTest(assert, [0.5, -0.5], [-0.5, -1.5]);
    this.doTest(assert, [-0.5, 0.5], [-1.5, -0.5]);
    this.doTest(assert, [-1, 0.9], [-2, -0.1]);
});

QUnit.test("Center and zoom", function(assert) {
    this.projection.setCenter([10, 5]);
    this.projection.setZoom(3);
    this.doTest(assert, [4.5, 1.5], [0.5, -0.5]);
    this.doTest(assert, [-0.6, -3], [-1.2, -2]);
    this.doTest(assert, [1.5, -1.5], [-0.5, -1.5]);
});

QUnit.module("Internal - to screen", $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.projection.setZoom(null);
        this.projection.setSize({ left: 200, top: 100, width: 800, height: 700 });
    },

    doTest: createInternalMethodTester("_toScreen")
}));

QUnit.test("Size is (400 300)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 400, height: 300 });
    this.doTest(assert, [-1, -1], [90, 30]);
    this.doTest(assert, [0, 0], [210, 180]);
    this.doTest(assert, [0.5, 0.75], [270, 292.5]);
});

QUnit.test("Size is (500 800)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 500, height: 800 });
    this.doTest(assert, [-1, -1], [10, 117.5]);
    this.doTest(assert, [0, 0], [260, 430]);
    this.doTest(assert, [0.5, 0.75], [385, 664.375]);
});

QUnit.test("Size is (200 200)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 200, height: 200 });
    this.doTest(assert, [-1, -1], [30, 30]);
    this.doTest(assert, [0, 0], [110, 130]);
    this.doTest(assert, [0.5, 0.75], [150, 205]);
});

// T127942
QUnit.test("Size is (400 300) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 400, height: 300 });
    this.doTest(assert, [-1, -1], [0, 50]);
    this.doTest(assert, [1, 1], [400, 250]);
});

// T127942
QUnit.test("Size is (600 800) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 600, height: 800 });
    this.doTest(assert, [-1, -1], [0, 250]);
    this.doTest(assert, [1, 1], [600, 550]);
});

// T127942
QUnit.test("Size is (200 200) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 200, height: 200 });
    this.doTest(assert, [-1, -1], [0, 50]);
    this.doTest(assert, [1, 1], [200, 150]);
});

QUnit.module("Internal - from screen", $.extend({}, environment, {
    beforeEach: function() {
        environment.beforeEach.apply(this, arguments);
        this.projection.setZoom(null);
    },

    doTest: createInternalMethodTester("_fromScreen")
}));

QUnit.test("Size is (400 300)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 400, height: 300 });
    this.doTest(assert, [60, 30], [-1.25, -1]);
    this.doTest(assert, [210, 180], [0, 0]);
    this.doTest(assert, [285, 292.5], [0.625, 0.75]);
});

QUnit.test("Size is (500 800)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 500, height: 800 });
    this.doTest(assert, [10, 180], [-1, -0.8]);
    this.doTest(assert, [260, 430], [0, 0]);
    this.doTest(assert, [385, 617.5], [0.5, 0.6]);
});

QUnit.test("Size is (200 200)", function(assert) {
    this.projection.setSize({ left: 10, right: 20, top: 30, bottom: 40, width: 200, height: 200 });
    this.doTest(assert, [10, 30], [-1.25, -1]);
    this.doTest(assert, [110, 130], [0, 0]);
    this.doTest(assert, [160, 205], [0.625, 0.75]);
});

// T127942
QUnit.test("Size is (400 300) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 400, height: 300 });
    this.doTest(assert, [0, 50], [-1, -1]);
    this.doTest(assert, [400, 250], [1, 1]);
});

// T127942
QUnit.test("Size is (600 800) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 600, height: 800 });
    this.doTest(assert, [0, 250], [-1, -1]);
    this.doTest(assert, [600, 550], [1, 1]);
});

// T127942
QUnit.test("Size is (200 200) / aspect ratio - 2", function(assert) {
    this.engine.ar = returnValue(2);
    this.projection.setSize({ left: 0, right: 0, top: 0, bottom: 0, width: 200, height: 200 });
    this.doTest(assert, [0, 50], [-1, -1]);
    this.doTest(assert, [200, 150], [1, 1]);
});

QUnit.module("projection");

QUnit.test("creation / validation", function(assert) {
    assert.strictEqual(projection(), null, "no arguments");
    assert.strictEqual(projection({}), null, "empty object");
});

QUnit.test("creation", function(assert) {
    var to = sinon.stub().returns([1000, 2000]),
        from = sinon.stub().returns([3000, 4000]);
    from.withArgs([0, 0]).returns([300, 400]);
    from.withArgs([-1, 0]).returns([10, 20]);
    from.withArgs([0, +1]).returns([30, 40]);
    from.withArgs([+1, 0]).returns([50, 60]);
    from.withArgs([0, -1]).returns([70, 80]);

    var proj = projection({
        aspectRatio: 3,
        to: to,
        from: from
    });

    assert.ok(proj instanceof projectionModule._TESTS_Engine, "instance type");

    assert.deepEqual(proj.project([1, 2]), [1000, -2000], "project");
    assert.deepEqual(to.lastCall.args, [[1, 2]], "source project");

    assert.deepEqual(proj.unproject([3, 4]), [3000, 4000], "unproject");
    assert.deepEqual(from.lastCall.args, [[3, -4]], "source unproject");

    assert.deepEqual(proj.source(), {
        aspectRatio: 3,
        to: to,
        from: from
    }, "source");
    assert.strictEqual(proj.original(), proj, "original");
    assert.strictEqual(proj.isInvertible(), true, "isinv");
    assert.strictEqual(proj.ar(), 3, "ar");
    assert.deepEqual(proj.center(), [300, 400], "center");
    assert.deepEqual(proj.min(), [10, 40], "min");
    assert.deepEqual(proj.max(), [50, 80], "max");
});

QUnit.test("creation / non invertible", function(assert) {
    var to = sinon.stub().returns([1000, 2000]);

    var proj = projection({ to: to });

    assert.ok(proj instanceof projectionModule._TESTS_Engine, "instance type");

    assert.deepEqual(proj.project([1, 2]), [1000, -2000], "project");
    assert.deepEqual(to.lastCall.args, [[1, 2]], "source project");

    assert.deepEqual(proj.unproject([3, 4]), [NaN, NaN], "unproject");

    assert.deepEqual(proj.source(), {
        to: to
    }, "source");
    assert.strictEqual(proj.original(), proj, "original");
    assert.strictEqual(proj.isInvertible(), false, "isinv");
    assert.strictEqual(proj.ar(), 1, "ar");
    assert.deepEqual(proj.center(), [NaN, NaN], "center");
    assert.deepEqual(proj.min(), [NaN, NaN], "min");
    assert.deepEqual(proj.max(), [NaN, NaN], "max");
});

QUnit.test("bounds", function(assert) {
    var proj = projection({
        aspectRatio: 3,
        to: function(arg) {
            return [arg[0] * 2, arg[1] * 3];
        },
        from: function(arg) {
            return [arg[0] + 10, arg[1] + 20];
        }
    });

    var newProj = proj.bounds([10, 20, 30, 40]);

    assert.ok(newProj instanceof projectionModule._TESTS_Engine, "instance type");
    assert.ok(newProj !== proj, "not same instance");
    assert.strictEqual(newProj.original(), proj, "original");
    assert.strictEqual(newProj.ar(), 3, "ar");
});

QUnit.test("aspectRatio", function(assert) {
    var proj = projection({
        aspectRatio: 3,
        to: function(arg) {
            return [arg[0] * 2, arg[1] * 3];
        },
        from: function(arg) {
            return [arg[0] + 10, arg[1] + 20];
        }
    });

    var newProj = proj.aspectRatio(4);

    assert.ok(newProj instanceof projectionModule._TESTS_Engine, "instance type");
    assert.ok(newProj !== proj, "not same instance");
    assert.strictEqual(newProj.original(), proj, "original");
    assert.strictEqual(newProj.ar(), 4, "ar");
    assert.deepEqual(newProj.project([10, 20]), proj.project([10, 20]), "project");
    assert.deepEqual(newProj.unproject([30, 40]), proj.unproject([30, 40]), "unproject");
});

$.each([
    { bounds: [-1.5, -3, 0.5, 1], center: [-0.5, -1], min: [-1.5, -3], max: [0.5, 1], project: [-1, 2], unproject: [-0.5, -1.5], message: "x equals y" },
    { bounds: [-1.5, -0.5, 0.5, 1.5], center: [-0.5, 0.5], min: [-1.5, -0.5], max: [0.5, 1.5], project: [-0.25, 0.75], unproject: [0.5, -0.25], message: "x is wider than y" },
    { bounds: [0.5, -3, 1.5, 1], center: [1, -1], min: [0.5, -3], max: [1.5, 1], project: [1.25, -0.5], unproject: [0.5, -0.5], message: "y is wider than x" },
    { bounds: [0.5, -3, 0.5, 1], center: [0.5, -1], min: [0.5, -3], max: [0.5, 1], project: [1, -2], unproject: [0.5, 0.5], message: "x is zero" },
    { bounds: [-1.5, -0.5, 0.5, -0.5], center: [-0.5, -0.5], min: [-1.5, -0.5], max: [0.5, -0.5], project: [-1, 1], unproject: [-0.5, -0.75], message: "y is zero" }
], function(_, data) {
    QUnit.test("bounds / " + data.message, function(assert) {
        var proj = projection({
            to: function(arg) {
                return [arg[0] / 2, arg[1] / 4];
            },
            from: function(arg) {
                return [arg[0] * 2, arg[1] * 4];
            }
        }).bounds(data.bounds);

        assert.deepEqual(proj.project(data.project), data.unproject, "project");
        assert.deepEqual(proj.unproject(data.unproject), data.project, "unproject");
        assert.deepEqual(proj.center(), data.center, "center");
        assert.deepEqual(proj.min(), data.min, "min");
        assert.deepEqual(proj.max(), data.max, "max");
    });
});

QUnit.test("add/get methods", function(assert) {
    var proj = createStubEngine();

    projection.add("tester-1", proj);

    assert.strictEqual(projection.get("tester-1"), proj);
});

QUnit.test("add/get methods / not valid", function(assert) {
    projection.add("tester-2", {});

    assert.strictEqual(projection.get("tester-2"), null);
});

QUnit.test("add/get methods / duplication", function(assert) {
    var proj = createStubEngine();

    projection.add("tester-3", proj).add("tester-3", createStubEngine());

    assert.strictEqual(projection.get("tester-3"), proj);
});

QUnit.module("Engines", {
    check: function(assert, data) {
        var project = projection.get(QUnit.config.current.testName).project,
            unproject = projection.get(QUnit.config.current.testName).unproject;
        $.each(data, function(_, item) {
            assert.arraysEqual(project(item.project), item.unproject, "project: " + item.project.join(" "));
            assert.arraysEqual(unproject(item.unproject), item.project, "unproject: " + item.unproject.join(" "), 1E-2);
        });
    }
});

QUnit.test("mercator", function(assert) {
    var X = 85.0511;
    this.check(assert, [
        { project: [-180, -X], unproject: [-1, +1] },
        { project: [-180, +X], unproject: [-1, -1] },
        { project: [+180, -X], unproject: [+1, +1] },
        { project: [+180, +X], unproject: [+1, -1] },
        { project: [-90, +45], unproject: [-0.5, -0.2805] },
        { project: [+45, -22.5], unproject: [+0.25, +0.1283] }
    ]);
});

QUnit.test("equirectangular", function(assert) {
    this.check(assert, [
        { project: [-180, -90], unproject: [-1, +1] },
        { project: [-180, +90], unproject: [-1, -1] },
        { project: [+180, -90], unproject: [+1, +1] },
        { project: [+180, +90], unproject: [+1, -1] },
        { project: [+270, -180], unproject: [+1.5, +2] },
        { project: [-90, +45], unproject: [-0.5, -0.5] },
        { project: [+45, -22.5], unproject: [+0.25, +0.25] }
    ]);
});

QUnit.test("lambert", function(assert) {
    this.check(assert, [
        { project: [-180, -90], unproject: [-1, +1] },
        { project: [-180, +90], unproject: [-1, -1] },
        { project: [+180, -90], unproject: [+1, +1] },
        { project: [+180, +90], unproject: [+1, -1] },
        { project: [+270, -60], unproject: [+1.5, 0.866] },
        { project: [-90, +45], unproject: [-0.5, -0.7071] },
        { project: [+45, -22.5], unproject: [+0.25, +0.3827] }
    ]);
});

QUnit.test("miller", function(assert) {
    this.check(assert, [
        { project: [-180, -90], unproject: [-1, +0.7332] },
        { project: [-180, +90], unproject: [-1, -0.7332] },
        { project: [+180, -90], unproject: [+1, +0.7332] },
        { project: [+180, +90], unproject: [+1, -0.7332] },
        { project: [+270, -70], unproject: [+1.5, +0.4715] },
        { project: [-90, +45], unproject: [-0.5, -0.2683] },
        { project: [+45, -22.5], unproject: [+0.25, +0.1271] }
    ]);
});

function createStubEngine() {
    return projection({
        to: function(coords) {
            return [(coords[0] - 40) / 20, (coords[1] + 20) / 10];
        },
        from: function(coords) {
            return [(coords[0] + 2) * 20, (coords[1] - 2) * 10];
        },
        aspectRatio: 0.8
    });
}

function createInternalMethodTester(methodName) {
    return function(assert, arg, expected) {
        var actual = this.projection[methodName](arg);
        assert.arraysEqual(actual, expected, arg.join(" "));
    };
}

QUnit.assert.arraysEqual = function(actual, expected, message, epsilon) {
    var i = 0,
        ii = expected.length,
        equal = true;
    epsilon = epsilon || 1E-4;
    for(; i < ii; ++i) {
        equal = Math.abs(actual[i] - expected[i]) <= epsilon;
        if(!equal) break;
    }
    this.pushResult({
        result: equal,
        actual: actual,
        expected: expected,
        message: message
    });
};
